package projects.android.sms.db;

import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;

import projects.android.sms.db.DBConstants.KeyType;
import projects.android.sms.object.SMS;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.telephony.PhoneNumberUtils;
import android.util.Log;

/**
 * Class designed as singleton pattern
 * 
 * @author Thomas Jaeger
 * @author Mario Kraml
 * @author Tassilo Posegga
 * @author Robert Schenkenfelder
 * 
 */
public class DBHandle extends SQLiteOpenHelper {

	private static final String TAG = "DBHandle";

	// singleton instance
	private static DBHandle instance;

	/**
	 * Expected to be called only one time and only through the {@link #getInstance(Context)} method.
	 * 
	 * @param context
	 */
	private DBHandle(Context context) {
		super(context, DBConstants.DATABASE_NAME, null, DBConstants.DATABASE_VERSION);
	}

	/**
	 * Returns the instance of the DBHandle. If this method is called the first time in life cycle, the handle is going
	 * to be constructed. Otherwise, the already available handle is returned. This is necessary to guarantee that only
	 * one instance of DBHandle exists.
	 * 
	 * @return The DBHandle instance.
	 */
	public static DBHandle getInstance(Context context) {
		if (instance == null) {
			instance = new DBHandle(context);
		}
		return instance;
	}

	// Creating tables
	@Override
	public void onCreate(SQLiteDatabase db) {
		db.execSQL(DBConstants.SmsTable.CREATE);
		db.execSQL(DBConstants.KeysTable.CREATE);
		db.execSQL(DBConstants.OwnKeyPairTable.CREATE);
	}

	// Upgrading database
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		// Drop older table if existed
		db.execSQL("DROP TABLE IF EXISTS " + DBConstants.SmsTable.TABLE_NAME);
		db.execSQL("DROP TABLE IF EXISTS " + DBConstants.KeysTable.TABLE_NAME);
		db.execSQL("DROP TABLE IF EXISTS " + DBConstants.OwnKeyPairTable.TABLE_NAME);

		// Create tables again
		onCreate(db);
	}

	/**
	 * Adds a new public key for a certain phone number to the database.
	 * 
	 * @param phoneNumber
	 *            Phone number of the conversation partner where the public key should be stored.
	 * @param key
	 *            A {@link RSAPublicKey} object representing the public key of the conversation partner with the given
	 *            phone number.
	 */
	public void addPublicKey(String phoneNumber, RSAPublicKey key) {
		SQLiteDatabase db = getWritableDatabase();

		String pubExp = key.getPublicExponent().toString(16);
		String mod = key.getModulus().toString(16);

		ContentValues values = new ContentValues();
		values.put(DBConstants.KeysTable.NUMBER, phoneNumber); // Contact Name
		values.put(DBConstants.KeysTable.PUBLIC_EXPONENT, pubExp);
		values.put(DBConstants.KeysTable.MODULUS, mod);

		// Inserting Row
		db.insert(DBConstants.KeysTable.TABLE_NAME, null, values);
		db.close(); // Closing database connection
	}

	/**
	 * Fetches the public key for a certain phone number from the database.
	 * 
	 * @param context
	 *            Application context.
	 * @param phoneNumber
	 *            Phone number of the conversation partner where the public key should be fetched.
	 * @return A {@link RSAPublicKey} object representing the public key of the conversation partner with the given
	 *         phone number.
	 */
	public RSAPublicKey getPublicKey(Context context, String phoneNumber) {
		SQLiteDatabase db = getWritableDatabase();

		String table = DBConstants.KeysTable.TABLE_NAME;
		String[] columns = { DBConstants.KeysTable.NUMBER, DBConstants.KeysTable.PUBLIC_EXPONENT,
				DBConstants.KeysTable.MODULUS };
		Cursor cursor = db.query(table, columns, null, null, null, null, null);

		try {
			while (cursor.moveToNext()) {
				String number = cursor.getString(cursor.getColumnIndex(DBConstants.KeysTable.NUMBER));
				if (PhoneNumberUtils.compare(context, phoneNumber, number)) {
					String pubExp = cursor.getString(cursor.getColumnIndex(DBConstants.KeysTable.PUBLIC_EXPONENT));
					String mod = cursor.getString(cursor.getColumnIndex(DBConstants.KeysTable.MODULUS));
					RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec(new BigInteger(mod, 16), new BigInteger(pubExp,
							16));
					KeyFactory keyFactory = KeyFactory.getInstance("RSA");
					return (RSAPublicKey) keyFactory.generatePublic(pubKeySpec);
				}
			}
		} catch (Exception e) {
			Log.e(TAG, "Exception in getOwnPublicKey", e);
		} finally {
			db.close();
		}
		return null;
	}

	/**
	 * Saves the own key pair to the database.
	 * 
	 * @param privateKey
	 *            Private key.
	 * @param publicKey
	 *            Public key.
	 */
	public void setOwnKeyPair(RSAPrivateCrtKey privateKey, RSAPublicKey publicKey) {
		SQLiteDatabase db = getWritableDatabase();

		// clear old key pair
		String tableName = DBConstants.OwnKeyPairTable.TABLE_NAME;
		db.delete(tableName, null, null);

		// insert private key
		String privMod = privateKey.getModulus().toString(16);
		String privPubExp = privateKey.getPublicExponent().toString(16);
		String privPrivExp = privateKey.getPrivateExponent().toString(16);
		String privPrimeP = privateKey.getPrimeP().toString(16);
		String privPrimeQ = privateKey.getPrimeQ().toString(16);
		String priPrimeExpP = privateKey.getPrimeExponentP().toString(16);
		String privPrimeExpQ = privateKey.getPrimeExponentQ().toString(16);
		String privCRT = privateKey.getCrtCoefficient().toString(16);

		ContentValues values = new ContentValues();
		values.put(DBConstants.OwnKeyPairTable.TYPE, KeyType.Private.getType());
		values.put(DBConstants.OwnKeyPairTable.MODULUS, privMod);
		values.put(DBConstants.OwnKeyPairTable.PUBLIC_EXPONENT, privPubExp);
		values.put(DBConstants.OwnKeyPairTable.PRIVATE_EXPONENT, privPrivExp);
		values.put(DBConstants.OwnKeyPairTable.PRIME_P, privPrimeP);
		values.put(DBConstants.OwnKeyPairTable.PRIME_Q, privPrimeQ);
		values.put(DBConstants.OwnKeyPairTable.PRIME_EXPONENT_P, priPrimeExpP);
		values.put(DBConstants.OwnKeyPairTable.PRIME_EXPONENT_Q, privPrimeExpQ);
		values.put(DBConstants.OwnKeyPairTable.CRT_COEFFICIENT, privCRT);
		db.insert(tableName, null, values);

		// insert public key
		String pubExp = publicKey.getPublicExponent().toString(16);
		String pubMod = publicKey.getModulus().toString(16);

		values = new ContentValues();
		values.put(DBConstants.OwnKeyPairTable.TYPE, KeyType.Public.getType());
		values.put(DBConstants.OwnKeyPairTable.PUBLIC_EXPONENT, pubExp);
		values.put(DBConstants.OwnKeyPairTable.MODULUS, pubMod);
		db.insert(tableName, null, values);

		db.close(); // Closing database connection
	}

	/**
	 * Fetches the own private key from the database.
	 * 
	 * @return A {@link RSAPrivateKey} object representing the private key of the user.
	 */
	public RSAPrivateKey getOwnPrivateKey() {
		SQLiteDatabase db = getWritableDatabase();

		String table = DBConstants.OwnKeyPairTable.TABLE_NAME;
		String[] columns = { DBConstants.OwnKeyPairTable.MODULUS, DBConstants.OwnKeyPairTable.PUBLIC_EXPONENT,
				DBConstants.OwnKeyPairTable.PRIVATE_EXPONENT, DBConstants.OwnKeyPairTable.PRIME_P,
				DBConstants.OwnKeyPairTable.PRIME_Q, DBConstants.OwnKeyPairTable.PRIME_EXPONENT_P,
				DBConstants.OwnKeyPairTable.PRIME_EXPONENT_Q, DBConstants.OwnKeyPairTable.CRT_COEFFICIENT };
		String selection = DBConstants.OwnKeyPairTable.TYPE + " = ?";
		String[] selectionArgs = { KeyType.Private.getType() };
		Cursor cursor = db.query(table, columns, selection, selectionArgs, null, null, null);

		try {
			if (cursor.moveToNext()) {
				String mod = cursor.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.MODULUS));
				String pubExp = cursor.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.PUBLIC_EXPONENT));
				String privExp = cursor.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.PRIVATE_EXPONENT));
				String primeP = cursor.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.PRIME_P));
				String primeQ = cursor.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.PRIME_Q));
				String primeExpP = cursor
						.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.PRIME_EXPONENT_P));
				String primeExpQ = cursor
						.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.PRIME_EXPONENT_Q));
				String crt = cursor.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.CRT_COEFFICIENT));
				RSAPrivateKeySpec privKeySpec = new RSAPrivateCrtKeySpec(new BigInteger(mod, 16), new BigInteger(
						pubExp, 16), new BigInteger(privExp, 16), new BigInteger(primeP, 16),
						new BigInteger(primeQ, 16), new BigInteger(primeExpP, 16), new BigInteger(primeExpQ, 16),
						new BigInteger(crt, 16));

				KeyFactory keyFactory = KeyFactory.getInstance("RSA");
				return (RSAPrivateKey) keyFactory.generatePrivate(privKeySpec);
			}
		} catch (Exception e) {
			Log.e(TAG, "Exception in getOwnPublicKey", e);
		} finally {
			db.close();
		}
		return null;
	}

	/**
	 * Fetches the own public key from the database.
	 * 
	 * @return A {@link RSAPublicKey} object representing the public key of the user.
	 */
	public RSAPublicKey getOwnPublicKey() {
		SQLiteDatabase db = getWritableDatabase();

		String table = DBConstants.OwnKeyPairTable.TABLE_NAME;
		String[] columns = { DBConstants.OwnKeyPairTable.PUBLIC_EXPONENT, DBConstants.OwnKeyPairTable.MODULUS };
		String selection = DBConstants.OwnKeyPairTable.TYPE + " = ?";
		String[] selectionArgs = { KeyType.Public.getType() };
		Cursor cursor = db.query(table, columns, selection, selectionArgs, null, null, null);

		try {
			if (cursor.moveToNext()) {
				String exp = cursor.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.PUBLIC_EXPONENT));
				String mod = cursor.getString(cursor.getColumnIndex(DBConstants.OwnKeyPairTable.MODULUS));
				RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec(new BigInteger(mod, 16), new BigInteger(exp, 16));
				KeyFactory keyFactory = KeyFactory.getInstance("RSA");
				return (RSAPublicKey) keyFactory.generatePublic(pubKeySpec);
			}
		} catch (Exception e) {
			Log.e(TAG, "Exception in getOwnPublicKey", e);
		} finally {
			db.close();
		}
		return null;
	}

	/**
	 * Adds a new text message to the database.
	 * 
	 * @param sms
	 *            The text message to be added to the database.
	 */
	public void addMessage(SMS sms) {
		SQLiteDatabase db = this.getWritableDatabase();

		ContentValues values = new ContentValues();
		values.put(DBConstants.SmsTable.MESSAGE, sms.getMessage()); // Contact Name
		values.put(DBConstants.SmsTable.NUMBER, sms.getPhonenumber()); // Contact Phone
		values.put(DBConstants.SmsTable.DATE, sms.getDate().getTime());
		int incoming = 0;
		if (sms.isIncoming()) {
			incoming = 1;
		}
		values.put(DBConstants.SmsTable.INCOMING, incoming); // incoming = 1, outgoing = 0

		// Inserting Row
		db.insert(DBConstants.SmsTable.TABLE_NAME, null, values);
		db.close(); // Closing database connection
	}

	/**
	 * Fetches all text messages from the database.
	 * 
	 * @return A list of all text messages.
	 */
	public ArrayList<SMS> getAllMessages() {
		SQLiteDatabase db = getWritableDatabase();

		ArrayList<SMS> messageList = new ArrayList<SMS>();

		String table = DBConstants.SmsTable.TABLE_NAME;
		String[] columns = { DBConstants.SmsTable.ID, DBConstants.SmsTable.MESSAGE, DBConstants.SmsTable.NUMBER,
				DBConstants.SmsTable.DATE, DBConstants.SmsTable.INCOMING };
		Cursor cursor = db.query(table, columns, null, null, null, null, null);

		try {
			while (cursor.moveToNext()) {
				SMS sms = new SMS();
				sms.setId(Integer.parseInt(cursor.getString(cursor.getColumnIndex(DBConstants.SmsTable.ID))));
				sms.setMessage(cursor.getString(cursor.getColumnIndex(DBConstants.SmsTable.MESSAGE)));
				sms.setPhonenumber(cursor.getString(cursor.getColumnIndex(DBConstants.SmsTable.NUMBER)));
				sms.setDate(Long.valueOf(cursor.getString(cursor.getColumnIndex(DBConstants.SmsTable.DATE))));
				if (Integer.valueOf(cursor.getString(cursor.getColumnIndex(DBConstants.SmsTable.INCOMING))) == 0) {
					sms.setIncoming(false);
				} else {
					sms.setIncoming(true);
				}
				messageList.add(sms);
			}
		} finally {
			db.close();
		}

		return messageList;
	}

	/**
	 * Fetches all text messages from a certain user with the given phone number.
	 * 
	 * @param phoneNumber
	 * @return A list of text messages associated to the given phone number.
	 */
	public ArrayList<SMS> getAllMessagesFromUser(String phoneNumber) {
		if (phoneNumber == null) {
			return null;
		}
		ArrayList<SMS> contactList = getAllMessages();
		for (int i = 0; i < contactList.size(); i++) {
			if (!PhoneNumberUtils.compare(phoneNumber, contactList.get(i).getPhonenumber())) {
				contactList.remove(i);
				i--;
			}
		}
		return contactList;
	}

	/**
	 * Deletes the given text message from the database.
	 * 
	 * @param sms
	 *            Text message which should be deleted.
	 */
	public void deleteMessage(SMS sms) {
		SQLiteDatabase db = this.getWritableDatabase();
		db.delete(DBConstants.SmsTable.TABLE_NAME, DBConstants.SmsTable.ID + " = ?",
				new String[] { String.valueOf(sms.getId()) });
		db.close();
	}
}
